YUI().use('app', 'io', 'json', 'querystring',  function (Y) {
    var User, UserList, Disease, DiseaseList, Gene, GeneList,
        Game, GameList, Round, RoundList, Action, ActionList,
        HomeView, DashView, GameView, DizeezRouter, DizeezApp;

    /**
     * Factory function for building up the synchronization layer for Dizeez
     *
     * @class DizeezSync
     * @constructor
     */

    function DizeezSync () {}
    
    DizeezSync.prototype = {
        /**
         * URL for the resource. 
         * Meant to be overwritten when actually making the request.
         *
         * @property dizeezURL
         * @default ""
         */

        dizeezURL: '/',

        /**
         * Root for the resource (usually used for collections).
         * Meant to be overwritten when actually making the request.
         *
         * @property dizeezRoot
         * @default "/"
         */
        
        dizeezRoot: '/',

        /**
         * Builds the URL where the resource data is to be collected.
         * Can be overwritten when actually making the request.
         *
         * @method buildURL
         */
        
        buildURL: function () {
            return this.dizeezURL;
        },

        /**
         * Builds the root where the resource collection is located.
         * Can be overwritten when actually making the request.
         *
         * @method buildRoot
         */
        
        buildRoot: function () {
            return this.dizeezRoot;
        },

        /**
         * Synchronizes our game state with the REST API.
         * Adds any desired filters or URL changes along the way.
         *
         * @method sync
         * @params action {String} Desired CRUD action
         * @params options {Object} Options to add to the XHR call
         * @params callback {Function} Callback function with failure/success
         */
        
        sync: function (action, options, callback) {
            Y.Lang.isFunction(callback) || (callback = function () {});
            var url,
                method;
                
            // We only use 'read', 'create', and 'update' in Dizeez
            if (action === 'read') {
                method = "GET";
                url = this.buildURL();
                if (options.user) {
                    url += ("&user=" + options.user);
                }
                if (options.not_user) {
                    url += ("&not_user=" + options.not_user);
                }
                if (options.info) {
                    url += ("&info=" + options.info);
                }
            } else if (action === 'create') {
                method = "POST";
                url = this.buildRoot();
                options.data = Y.JSON.stringify(this.toJSON());
            } else if (action === 'update') {
                method = "PUT";
                url = this.buildURL();
                options.data = Y.JSON.stringify(this.toJSON());
            } else {
                return callback('error: method not supported')
            }

            Y.io(url, {
                method: method,
                data: options.data,
                headers: {
                    'Content-Type': 'application/json'
                },
                on: {
                    success: function (id, res, args) {
                        callback(null, res);
                    },
                    failure: function (id, res, args) {
                        callback(res.statusText);
                    }
                }                
            });
        },

        /**
         * Parses the resulting data from the API.
         * Can be overwritten during the actual request.
         *
         * @method parse
         * @param res {String} REST API response
         */

        parse: function (res) {
            return Y.JSON.parse(res.responseText);
        }
    }
    
    /* Models */

    /**
     * Client-side Model for our User data structure
     *
     * @class User
     * @constructor
     */
    
    User = Y.Base.create('user', Y.Model, [DizeezSync], {
        dizeezURL: '/users/{id}',
        
        dizeezRoot: '/users',
        
        buildURL: function () {
            return Y.Lang.sub(this.dizeezURL, {
                id: this.getAsURL('id')
            });
        }
    }, {
        ATTRS: {
            id: { value: null },
            name: { value: null },
            games_played: { value: 0 },
            points: { value: 0},
            date: { value: null }
        }
    });

    /**
     * Client-side Model List for our User data structure.
     *
     * @class UserList
     * @constructor
     */
    
    UserList = Y.Base.create('userList', Y.ModelList, [DizeezSync], {
        model: User,
        dizeezURL: '/users'
    });

    /**
     * Client-side Model for our Disease data structure.
     *
     * @class Disease
     * @constructor
     */
    
    Disease = Y.Base.create('disease', Y.Model, [DizeezSync], {
        dizeezURL: '/diseases/{id}', 
        
        dizeezRoot: '/diseases',
        
        buildURL: function () {
            return Y.Lang.sub(this.dizeezURL, {
                id: this.getAsURL('id')
            });
        }
    }, {
        ATTRS: {
            id: { value: null },
            name: { value: null },
            links: { value: null }
        }
    });

    /**
     * Client-side Model List for our Disease data structure.
     *
     * @class DiseaseList
     * @constructor
     */
    
    DiseaseList = Y.Base.create('diseaseList', Y.ModelList, [DizeezSync], {
        model: Disease,
        dizeezURL: '/diseases',
        buildURL: function () {
            return this.dizeezURL + '?range=6&contains=' + this.get('searchTerm');
        }
    }, {
        ATTRS: {
            searchTerm: { value: null }
        }
    });
    
    /**
     * Client-side Model for our Gene data structure.
     *
     * @class Gene
     * @constructor
     */

    Gene = Y.Base.create('gene', Y.Model, [DizeezSync], {
        dizeezURL: '/genes/{id}',
        
        dizeezRoot: '/genes',
        
        buildURL: function () {
            return Y.Lang.sub(this.dizeezURL, {
                id: this.getAsURL('id')
            });
        }
    }, {
        ATTRS: {
            id: { value: null },
            name: { value: null },
            symbol: { value: null },
            links: { value: null }
        }
    });
    
    /**
     * Client-side Model List for our Gene data structure.
     *
     * @class GeneList
     * @constructor
     */

    GeneList = Y.Base.create('geneList', Y.ModelList, [DizeezSync], {
        model: Gene,
        dizeezURL: '/genes',
        buildURL: function () {
            return this.dizeezURL + '?range=6&contains=' + this.get('searchTerm');
        }
    }, {
        ATTRS: {
            searchTerm: { value: null }
        }
    });

    /**
     * Client-side Model for our Game data structure.
     * 
     * @class Game
     * @constructor
     */
    
    Game = Y.Base.create('game', Y.Model, [DizeezSync], {
        dizeezURL: '/games/{game_id}',
        
        dizeezRoot: '/games',
        
        buildURL: function () {
            return Y.Lang.sub(this.dizeezURL, {
                game_id: this.getAsURL('id')
            });
        }
    }, {
        ATTRS: {
            id: { value: null },
            points: { value: 0 },
            rounds: { value: [] },
            identifiers: { value: null },
            types: { value: null },
            players: { value: [] },
        }
    });

    /**
     * Client-side Model List for our Game data structure.
     *
     * @class GameList
     * @constructor
     */
    
    GameList = Y.Base.create('gameList', Y.ModelList, [DizeezSync], {
        model: Game,
        dizeezURL: '/games?range=4'
    });

    /**
     * Client-side Model for our Round data structure.
     *
     * @class Round
     * @constructor
     */
    
    Round = Y.Base.create('round', Y.Model, [DizeezSync], {
        dizeezURL: '/games/{game_id}/rounds/{round_id}',
        
        dizeezRoot: '/games/{game_id}/rounds',
        
        buildURL: function () {
            var idData = this.get('id').split('_');
            return Y.Lang.sub(this.dizeezURL, {
                game_id: idData[0],
                round_id: idData[1]
            });
        },
        
        buildRoot: function () {
            return Y.Lang.sub(this.dizeezRoot, {
                game_id: this.get('game_id')
            });
        }
    }, {
        ATTRS: {
            id: { value: null },
            players: { value: null },
            complete: { value: false },
            turn: { value: null },
            guess_target: { value: null },
            actions: { value: null },
            points: { value: 0 }    
        }
    });

    /**
     * Client-side Model List for the Round data structure.
     *
     * @class RoundList
     * @constructor
     */
    
    RoundList = Y.Base.create('roundList', Y.ModelList, [DizeezSync], {
        model: Round,
        
        dizeezURL: '/games/{game_id}/rounds',
        
        buildURL: function () {
            return Y.Lang.sub(this.dizeezURL, {
                game_id: this.get('game_id')
            });
        }
    }, {
        ATTRS: {
            game_id: { value: null }
        }
    });

    /**
     * Client-side Model for the Action data structure.
     *
     * @class Action
     * @constructor
     */
    
    Action = Y.Base.create('action', Y.Model, [DizeezSync], {
        dizeezURL: '/games/{game_id}/rounds/{round_id}/actions/{action_id}',
        
        dizeezRoot: '/games/{game_id}/rounds/{round_id}/actions',
        
        buildURL: function () {
            var idData = this.get('id').split('_');
            return Y.Lang.sub(this.dizeezURL, {
                game_id: idData[0],
                round_id: idData[1],
                action_id: idData[2]
            }) + '?method=' + this.get('method');
        },
        
        buildRoot: function () {
            var idData = this.get('idData').split('_');
            return Y.Lang.sub(this.dizeezRoot, {
                game_id: idData[0],
                round_id: idData[1]
            }) + '?method=' + this.get('method');
        }
    }, {
        ATTRS: {
            id: { value: null },
            value: { value: null },
            type: { value: null },
            time: { value: null }
        }
    });

    /**
     * Client-side Model List for the Action data structure.
     *
     * @class ActionList
     * @constructor
     */
    
    ActionList = Y.Base.create('actionList', Y.ModelList, [DizeezSync], {
        model: Action,
    
        dizeezURL: '/games/{game_id}/rounds/{round_id}/actions',
        
        buildURL: function () {
            return Y.Lang.sub(this.dizeezURL, {
                game_id: this.get('game_id'),
                round_id: this.get('round_id')
            }) + '?method=' + this.method;
        }
    }, {
        ATTRS: {
            game_id: { value: null },
            round_id: { value: null },
            method: { value: null }
        }
    });
    
    /* Views */

    /**
     * The main gameplay and UI for Dizeez itself.
     * Composed of several templates that make up each individual UI element.
     * Players continuously provide either hints or guesses until the guesser
     * is able to guess what the answer is, or a pass is made.
     *
     * The game last for five rounds, with each correct answer being worth
     * the same amount of points.
     *
     * The hinter and guesser roles alternate between the two players as
     * each round continues.
     *
     * @class GameView
     * @constructor
     */
    
    GameView = Y.Base.create('gameView', Y.View, [], {

        // User score and games played data
        userTemplate: Y.one('#user-template').getContent(),

        // Main game interface, storing hints and guesses
        gameTemplate: Y.one('#game-template').getContent(),

        // Element showing successful guesses and passes
        scoreTemplate: Y.one('#score-template').getContent(),

        // Message to be displayed on correct match
        winTemplate: Y.one('#correct-template').getContent(),

        // Message to be displayed on pass or time out
        loseTemplate: Y.one('#wrong-template').getContent(),

        // Results to be displayed at end of game
        resultsTemplate: Y.one('#results-template').getContent(),
        
        // Input box template to be shown when guessing
        inputTemplate: Y.one('#input-template').getContent(),

        // Message to be displayed when waiting for the other player
        waitTemplate: Y.one('#wait-template').getContent(),
       
        // General message template
        msgTemplate: Y.one('#msg-template').getContent(),
        
        // Auto-complete UI element to be displayed
        acTemplate: Y.one('#ac-template').getContent(),
        
        // Each individual action to be displayed on the screen
        actionTemplate: Y.one('#action-template').getContent(),

        // The countdown timer template
        timerTemplate: Y.one('#timer-template').getContent(),
        
        events: {
            // Send an AJAX request to autocomplete the user's input
            '#dizeez-input': {
                keydown: 'autoComplete'
            },
            // Add the hint/guess to the main content layer when clicked
            '.ac-item': {
                click: 'addAction'
            }
        },

        /**
         * Initializes the GameView, given a Game Model and Round Model
         *
         * @method initializer
         */
        
        initializer: function () {
            var game     = this.get('game'),
                round    = this.get('round'),
                self     = this;

            // Load the most updated data from the database
            round.load(function (err, res) {
            
            Y.one('#main-input').setContent(self.inputTemplate);

            // Sets up the goal and all necessary Models
            var goalID   = parseInt(round.get('guess_target')),
                target   = self.target   = new Disease({id: goalID})
                hints    = self.hints    = new ActionList({method: 'hint'}),
                guesses  = self.guesses  = new ActionList({method: 'guess'}),
                diseases = self.diseases = new DiseaseList(),
                genes    = self.genes    = new GeneList();
               
                // Sets up all change handlers to handle user interactions
                hints.after('add', self.addHint, self);
                guesses.after('add', self.addGuess, self);
                
                diseases.after('reset', self.resetChoices, self);
                genes.after('reset', self.resetChoices, self);

                round.after('load', self.changeTurn, self);
                game.after('load', self.loadRound, self);

                // Set up the handler for the pass button
                Y.one('#pass-btn').on('click', function (e) {
                    round.set('complete', true);
                    round.save(function () {
                        self.endRound({correct: false, role: "guesser"});
                    });
                });

                // Render the screen and begin the game
                target.load(function (err, res) {
                    // Start the clock and data polling
                    self.startTimer();
                    self.pollData();
                    self.render(target);
                });
            });
        },
        
        /**
         * The main method to render and keep the UI updated.
         *
         * @method render
         */

        render: function (target) {

            // Get all necessary models and DOM elements
            var container        = this.get('container'),
                player           = this.get('player'),
                round            = this.get('round'),
                target           = target,
                hintData         = round.get('actions.hints'),
                guessData        = round.get('actions.guesses'),
                inputBox         = container.one('#main-input'),
                notificationBox  = container.one('#notifications'),
                role = (round.get('players.hinter') == player.get('name')) ? 'hint' : 'guess';

            container.one('#main-container').setContent(this.gameTemplate);

            this.updateScores();

            // Provide a different message depending on whether the player
            // is either the guesser or the hinter.

            if (role === 'guess') {
                notificationBox.setContent(Y.Lang.sub(this.msgTemplate, {
                    message: "Guess the diseases from the hints!"    
                }));
            } else if (role === 'hint') {
                notificationBox.setContent(Y.Lang.sub(this.msgTemplate, {
                    message: "Target Disease:  " + target.get('name')
                }));
            }

            // Add new data to the screen if available
            if (hintData) {
                this.hints.add(hintData);
            }
            if (guessData) {
                this.guesses.add(guessData);
            }
        },

        /**
         * Start the timer to constantly decrement until it hits zero,
         * where it'll end the round.  Default time is 120 seconds.
         *
         * @method startTimer
         */

        startTimer: function () {
            var time = 120,
                timer = Y.one('#timer'),
                self = this;

            var timerID = window.setInterval(function () {
                timer.setContent(Y.Lang.sub(self.timerTemplate, {
                    time: time--
                }));
                if (time < 0) {
                    window.clearInterval(timerID);
                    self.endRound({correct: false, role: "guesser"});
                }
            }, 1000);

            this.set('timerID', timerID);
        },

        /**
         * Properly capitalizes the desired object with title case.
         *
         * @method toTitleCase
         * @params str {String} String to be capitalized
         * @example 'hello world' -> 'Hello World'
         */

        toTitleCase: function (str) {
            return str.replace(/\w\S*/g, function (txt) {
                return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
            });
        },

        /**
         * Updates the scores in the scoring element and user element.
         *
         * @method updateScores
         */

        updateScores: function () {
            var container = this.get('container'),
                scores    = this.get('scores');        
    
            container.one('#score').setContent(Y.Lang.sub(this.scoreTemplate, {
                "score-1": scores[0],
                "score-2": scores[1],
                "score-3": scores[2],
                "score-4": scores[3],
                "score-5": scores[4]
            }));
        },

        /**
         * Takes the users' input and performs an auto-completing search 
         * on their input data.
         *
         * @method autoComplete
         * @param e {KeyEvent} The keydown event that triggers this handler
         */
        
        autoComplete: function (e) {
            var input     = Y.one('#dizeez-input').get('value'),
                round     = this.get('round'),
                player    = this.get('player')
                selection = (round.get('players.hinter') == player.get('name')) ? this.genes : this.diseases;
            selection.set('searchTerm', input);
            selection.load();
        },

        /**
         * Adds a hint Action to the main gameplay area
         *
         * @method addHint
         * @params e {MouseEvent} Click event triggering the handler
         */
        
        addHint: function (e) {
            var hintContainer = this.get('container').one('#left-list'),
                hintAction = e.model;

            hintContainer.appendChild(Y.Lang.sub(this.actionTemplate, {
                name: hintAction.get('value')
            }));
        },

        /**
         * Adds a guess Action to the main gameplay area
         *
         * @method addGuess
         * @params e {MouseEvent} Click event triggering the handler
         */

        addGuess: function (e) {
            var guessContainer = this.get('container').one('#right-list'),
                guessAction = e.model,
                target = this.target,
                round = this.get('round')
                guesses = round.get('actions.guesses');

            guessContainer.appendChild(Y.Lang.sub(this.actionTemplate, {
                name: guessAction.get('value')
            }));

            if (guessAction.get('value') == target.get('name')) {
                this.endRound({correct: true, role: "guesser"});
            }
        },

        /**
         * Adds an Action Model to the database and hints/guesses Model Lists.
         * Provides the main synchronization technique for Dizeez.
         *
         * @method addAction
         * @params e {MouseEvent} Click event that triggers the handler.
         */

        addAction: function (e) {
            var game       = this.get('game'),
                round      = this.get('round'),
                player     = this.get('player'),
                opponent   = this.get('opponent'),
                target     = this.target,
                actionType = (round.get('players.hinter') == player.get('name')) ? 'gene' : 'disease',
                selection  = (actionType == 'gene') ? this.hints : this.guesses,
                newMessage = Y.one('#main-input'),
                newValue   = e.target.getHTML(),
                self       = this;
   
            // Get correct data regardless of element clicked        
            if (e.target.hasClass('ac-item')) {
                newValue = e.target.one('a').getHTML();
            }

            // Create a new Action Model depending on the data obtained
            selection.create({
                idData: round.get('id'),
                value: newValue,
                type: actionType,
                method: selection.get('method')
            });

            // If we have the correct guess, finish the round
            if (newValue == target.get('name')) {
                round.set('complete', true);
            }
          
            // Save the Round's new data to our sync layer  
            round.save(function (err, res) {
                if (!round.get('complete')) {
                    // Continue our polling if the round hasn't finished
                    self.pollData();
                }
            });

            // Clear the current auto-complete field
            Y.one('#autocomplete').empty();
            Y.one('#dizeez-input').set('value', '');
        },

        /**
         * Update the player's data and score in the user template.
         *
         * @method updateUser
         */

        updateUser: function () {
            var user = this.get('player');

            Y.one('#user-info').setContent(Y.Lang.sub(this.userTemplate, {
                name: user.get('name'),
                picture: user.get('avatar'),
                points: user.get('points'),
                games_played: user.get('games_played')
            }));
        },

        /**
         * Ends the round, or otherwise continues to re-render the main
         * gameplay content area.
         *
         * @method changeTurn
         * @params e {ChangeEvent} Change event that triggers this handler
         */

        changeTurn: function (e) {
            var round = this.get('round'),
                hints = round.get('actions.hints'),
                guesses = round.get('actions.guesses'),
                player = this.get('player'),
                hintContainer = Y.one('#left-list'),
                guessContainer = Y.one('#right-list');

            // End the round and pass a success or failure result
            if (round.get('complete')) {
                if (round.get('points') === 10) {
                    this.endRound({correct: true, role: "hinter"});
                } else {
                    this.endRound({correct: false, role: "hinter"});
                }
            }

            
            // Otherwise, continue to re-render the main game area            
            hintContainer.empty();
            guessContainer.empty();
            for (var i = 0; i < hints.length; i++) {
                hintContainer.appendChild(Y.Lang.sub(this.actionTemplate, {
                    name: hints[i].value
                }));
            }

            for (var j = 0; j < guesses.length; j++) {
                guessContainer.appendChild(Y.Lang.sub(this.actionTemplate, {
                    name: guesses[j].value
                }));
            }
        },

        /**
         * Loads the current Round data from an in-progress Game.
         * Allows users to pick back up a game from a previous state.
         * 
         * May be deprecated in the future to the real-time version of Dizeez,
         * but currently still needed to join a new game.
         *
         * @method loadRound
         * @params e {ChangeEvent} Change event that triggers the handler
         */

        loadRound: function (e) {
            var game   = this.get('game'),
                rounds = game.get('rounds'),
                lastRound = rounds[rounds.length - 1];

            if (!lastRound.complete) {
                window.clearInterval(this.get('gameID'));
                window.clearInterval(this.get('timerID'));
                this.set('round', new Round(lastRound));
                this.initializer();
            }
        },

        /**
         * Reloads the hint/guess templates after the autocomplete loads in
         * the new list of Gene/Disease Models.
         *
         * @method resetChoices
         * @params e {ResetEvent} Reset event that triggers the handler
         */

        resetChoices: function (e) {
            var acDisplay = Y.one('#autocomplete'),
                template = this.acTemplate,
                newModels = e.models;
           
            acDisplay.empty();
            newModels.map(function (model) {
                acDisplay.appendChild(Y.Lang.sub(template, {
                    name: model.get('name')
                }));
            });            
        },

        /**
         * Continuously polls the Round API for updates to the current Round,
         * based on the current game ID and round ID.
         *
         * @method pollData
         */
        
        pollData: function () {
            var self = this,
                intervalID;
               
            window.clearInterval(this.get('gameID'));
            window.clearInterval(this.get('pollID'));

            intervalID = window.setInterval(function () {
                self.get('round').load();
            }, 1000);

            this.sFet('pollID', intervalID);
        },

        /**
         * Continously polls the Game API for updates to the current Game,
         * based on the current game ID. 
         * 
         * @method pollGame
         */

        pollGame: function () {
            var self = this,
                intervalID;

            window.clearInterval(this.get('pollID'));
            window.clearInterval(this.get('gameID'));

            intervalID = window.setInterval(function () {
                    self.get('game').load();
                }, 2500);

            this.set('gameID', intervalID);
        },

        /**
         * Begins the round once both the player and opponent's User Model
         * has been loaded into the View.  Assigns each player as either a 
         * hinter or guesser role.
         * 
         * @method startround
         */

        startRound: function () {
            var game = this.get('game'),
                player = this.get('player'),
                opponent = this.get('opponent'),
                oldRound = this.get('round'),
                newRound,
                self = this;

            // Only start the round if both the player and opponent are there
            if (player && opponent) {
                // Set up the first round
                newRound = new Round({
                    players: {
                        hinter: oldRound.get('players.guesser'),
                        guesser: oldRound.get('players.hinter')
                    },
                    // Turn should be deprecated
                    turn: oldRound.get('players.guesser'),
                    game_id: game.get('id')
                });
                
                newRound.save(function () {
                    self.set('round', newRound);
                    self.initializer();
                });
            } 
        },

        /**
         * Ends the round, sets the score, and sets up the next round of new
         * guessers and hinters.  Updates both players' score in the database.
         * If we've reached all of the rounds, ends the game.
         *
         * @method endRound
         * @params data {Object} Object containing the pass/fail and player data
         */

        endRound: function (data) {
            var round  = this.get('round'),
                player = this.get('player'),
                numRounds = this.get('numRounds'),
                scores  = this.get('scores'),
                playerScore = player.get('points'),
                self = this;

            // Clear all of the timers, because otherwise the game breaks
            window.clearInterval(this.get('pollID'));
            window.clearInterval(this.get('gameID'));
            window.clearInterval(this.get('timerID'));

            // Clear out our input screen
            Y.one('#main-input').empty();
            if (data.correct) {
                // If correct, update the user's score
                Y.one('#notifications').setContent(this.winTemplate);
                scores[numRounds] = 'score-correct';
                round.set('points', 10);
                player.set('points', playerScore + 10);
                this.updateUser();
                round.save();
            } else {
                // Otherwise, don't affect the user model at all
                Y.one('#notifications').setContent(this.loseTemplate);
                scores[numRounds] = 'score-wrong';
            }

            // Set the game score and number of rounds passed
            this.set('scores', scores);
            this.set('numRounds', numRounds + 1);

            // End the game if we've reached five rounds
            if (this.get('numRounds') >= 5) {
                this.endGame();        
            } else {
                // Hacky way of switching player roles
                if (data.role === 'guesser') {
                    this.startRound();
                } else {
                    this.pollGame();
                }
            }
        },

        /**
         * Ends the game, sets the final score, and synchronizes any
         * remaining data that needs to be cleaned up.
         * 
         * @method endGame
         */
        
        endGame: function () { 
            var finalScore = 0,
                scores = this.get('scores'),
                round = this.get('round'),
                game = this.get('game'),
                player = this.get('player'),
                gamesPlayed = player.get('games_played'),
                mainContainer = Y.one('#main-container'),
                scoreContainer = Y.one('#score'),
                self = this;

            this.updateScores();

            // Calculates the final score
            for (var i = 0; i < 5; i++) {
                if (scores[i] === 'score-correct') {
                    finalScore += 10;
                }
            }

            // Cleans up the player data and increments games played
            player.set('games_played', gamesPlayed + 1);
            player.save();

            // Last round is complete
            round.set('info', 'complete');
            round.save(function () {
                // Game is now complete and points are set
                game.set('info', 'complete');
                game.set('points', finalScore);
                game.save(function () {
                    mainContainer.empty();
                    // Set up the results screen with our final data
                    mainContainer.setContent(Y.Lang.sub(self.resultsTemplate, {
                        score: finalScore,
                        total: player.get('points')
                    }));
                    Y.one('#return-button').on('click', function () {
                        mainContainer.empty();
                        scoreContainer.empty();
                        // Return button sends player back to the Dash View
                        new DashView({user: player});
                    });
                });
            });
        }
    } , {
        ATTRS: {
            // Main container (full page app)
            container: {
                valueFn: function () { return Y.one('body'); }
            },

            // ID for the round polling
            pollID: {
                value: null
            },

            // ID for the game polling
            gameID: {
                value: null
            },

            // Score array for generating templates
            scores: {
                value: ['', '', '', '', '']
            },

            // Number of rounds that have passed
            numRounds: {
                value: 0
            }
        }
    });

    /* HomeView */

    /**
     * A simple introductory message and a button that signs the user in
     * using the OAuth 2.0 technique with their Google Account.
     * Most of the methods here are simply for a OAuth 2.0 consumer,
     * and simplifies out many of the data methods.
     *
     * @class HomeView
     * @constructor
     */

    HomeView = Y.Base.create('homeView', Y.View, [], {
        // The template for the button
        buttonTemplate: Y.one('#button-template').getContent(),
        
        /**
         * Initializer that only renders the page.
         *
         * @method initializer
         */

        initializer: function () {
            this.render();
        },
        
        /**
         * Renders the introductory message and prepares the login button
         * for the user.  If the user already has a hash-based OAuth token,
         * then we extract out the data to determine the user data, and 
         * immediately render out the logged-in Dash View.
         *
         * @method render
         */

        render: function () {
            var self = this,
                userData, newUser;
            
            // Set up the default template and content
            Y.one('#main-container').setContent(this.buttonTemplate);

            // Click to begin the OAuth process
            Y.one('#login-button').on('click', function (e) {
                self.requestToken(e);
            });

            // If we already have an OAuth token
            if (location.hash) {
                var params = this.parseToken(location.hash);
                this.accessUserData(params.access_token, function (data) {
                    Y.io('/users?google_id=' + data.id, {
                        on: {
                            success: function (id, res) {
                                var users = Y.JSON.parse(res.responseText);
                                // If they're already in the database
                                if (users.length > 0) {
                                    self.syncExistingUser(users[0]);
                                // Otherwise create a new user!
                                } else {
                                    self.createNewUser(data);
                                }
                            }
                        }
                    });
                });
            }
        },

        /**
         * View transition used when we have a user that has played Dizeez 
         * before, and so we have all of their data already.
         *
         * @method syncExistingUser
         * @params user {Object} Hash of user data
         */

        syncExistingUser: function (user) {
            var existingUser = new User(user);
            Y.one('#main-container').empty();
            new DashView({user: existingUser});
        },

        /**
         * View transition used when we have a user that is playing Dizeez for
         * the first time, and we need to add their Google data to the 
         * database for processing.
         *
         * @method createNewUser
         * @params data {Object} Hash of user data
         */

        createNewUser: function (data) {
            // Set a default avatar if they don't have one already
            if (!data.picture) {
                data.picture = "https://lh4.googleusercontent.com/-uVnge7-2YPs/AAAAAAAAAAI/AAAAAAAAAAA/V8Aaqh_o_yE/s250-c-k/photo.jpg"
            }

            // Create the new User Model
            var newUser = new User({
                name: data.given_name,
                google_id: data.id,
                avatar: data.picture
            }),
                self = this;

            // Save the new User Model to the REST API, and then generate
            // the Dash View as in the previous method
            newUser.save(function (err, res) {
                var userId = Y.JSON.parse(res.responseText)['id'];
                newUser.set('id', userId);
                self.get('container').one('#login-button').remove();
                new DashView({user: newUser});
            });
        },

        /**
         * Requests the OAuth token from the Google API, using our pre-set data.
         *
         * @method requestToken
         */

        requestToken: function (e) {
            var url = "https://accounts.google.com/o/oauth2/auth?",
                params = {
                    response_type: 'token',
                    client_id: '234640891980.apps.googleusercontent.com',
                    redirect_uri: 'http://ec2-23-22-178-49.compute-1.amazonaws.com',
                    scope: 'https://www.googleapis.com/auth/userinfo.profile',
                };

            window.location = url + Y.QueryString.stringify(params);   
        },

        /**
         * Parses the resulting query string into our desired components.
         *
         * @method parseToken
         * @params token {String} Query string data
         * @return params {Object} The parsed query data
         */

        parseToken: function (token) {
            var params = {}, queryString = token.substring(1),
            regex = /([^&=]+)=([^&]*)/g, m;
            while (m = regex.exec(queryString)) {
                params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
            }
            return params;
        },

        /**
         * Sends the token over to the Google API for verification, and
         * receives the validation response.
         *
         * @method validateToken
         * @params token {String} Access token from OAuth
         * @params callback {Function} Callback function with the result data
         */

        validateToken: function (token, callback) {
            Y.io('https://www.googleapis.com/oauth2/v1/tokeninfo', {
                data: 'access_token=' + token,
                on: {
                    success: function (id, res, args) {
                        callback(Y.JSON.parse(res.responseText));
                    }
                }
            });
        },

        /**
         * Grabs the user's Google information from the Google API once the 
         * access token has been obtained.
         * 
         * @method accessUserData
         * @params token {String} Access token from OAuth
         * @params callback {Function} Callback function with the result data
         */

        accessUserData: function (token, callback) {
            Y.io('https://www.googleapis.com/oauth2/v1/userinfo', {
                data: 'access_token=' + token,
                on: {
                    success: function (id, res, args) {
                        callback(Y.JSON.parse(res.responseText));
                    }
                }
            });
        }
    }, {
        ATTRS: {
            // View container (full page app)
            container: {
                valueFn: function () { return Y.one('body'); }
            }
        }
    });

    /**
     * A dashboard view for the logged-in user that contains areas to check 
     * user statistics, set up new games, or join existing games.
     * 
     * Game notifications can also be set at this area, to announce updates to
     * Dizeez or other Su Lab properties.
     *
     * @class DashView
     * @constructor
     */

    DashView = Y.Base.create('dashView', Y.View, [], {
        userTemplate: Y.one('#user-template').getContent(),

        dashTemplate: Y.one('#dash-template').getContent(),

        startTemplate: Y.one('#start-template').getContent(),

        standbyTemplate: Y.one('#standby-template').getContent(),

        browseBtnTemplate: Y.one('#browse-btn-template').getContent(),

        selfBtnTemplate: Y.one('#self-btn-template').getContent(),

        browseTemplate: Y.one('#browse-template').getContent(),

        existTemplate: Y.one('#exist-template').getContent(),

        msgTemplate: Y.one('#msg-template').getContent(),

        messageBox: Y.one('#notifications'),

        initializer: function () {
            this.set('gameList', new GameList());
            this.set('mode', 'self');

            var gameList = this.get('gameList'),
                user     = this.get('user'),
                self     = this;

            this.set('opponent', new User());
            this.set('userQuery', user.get('id'));
            this.set('infoQuery', 'start');
        
            this.pollForData();
            this.setBrowseHandler();

            gameList.load({user: user.get('id'), info: 'start'}, function () {
                self.render();
            });
        },

        render: function () {
            var user = this.get('user'),
                gameList = this.get('gameList'),
                mode = this.get('mode'),
                mainContainer = Y.one('#main-container'),
                messageBox = this.messageBox,
                self = this;

            Y.one('#user-info').setContent(Y.Lang.sub(this.userTemplate, {
                name: user.get('name'),
                picture: user.get('avatar'),
                points: user.get('points'),
                games_played: user.get('games_played')
            }));

            messageBox.setContent(Y.Lang.sub(this.msgTemplate, {
                message: "Welcome to Dizeez! Play and help science!"
            }));

            for (var i = 0; i < 4; i++) {
                var game = gameList.item(i),
                    dash = mainContainer.appendChild(this.dashTemplate);
                if (game) {
                    if (game.get('info') === 'start') {
                        var players = game.get('players'),
                            gameId  = game.get('id');
                        dash.setHTML(Y.Lang.sub(this.existTemplate, {
                            game_id: gameId,
                            player_one: players[0],
                            player_two: players[1]
                        }));
                        dash.on('click', function (e) {
                            self.joinStartedGame(e);
                        });
                    } else if (game.get('info') === 'standby') {
                        if (mode === 'self') {
                            dash.appendChild(this.standbyTemplate);
                        } else if (mode === 'browse') {
                            var players = game.get('players');
                            dash.appendChild(Y.Lang.sub(this.browseTemplate, {
                                player: players[0],
                                game_id: game.get('id')
                            }));
                            dash.on('click', function (e) {
                                self.joinStandbyGame(e);
                            });
                        }
                    }
                } else {
                    dash.appendChild(this.startTemplate);
                    dash.on('click', function () {
                        self.createStandbyGame();
                    });
                }
            }
        },

        setBrowseHandler: function () {
            var mainInput = Y.one('#main-input'),
                self = this;

            mainInput.empty();
            mainInput.appendChild(this.browseBtnTemplate);
            Y.one('.browse-btn').on('click', function () {
                self.setSelfHandler();
                self.showStandbyGames();
            });
        },

        setSelfHandler: function () {
            var mainInput = Y.one('#main-input'),
                self = this;

            mainInput.empty();
            mainInput.appendChild(this.selfBtnTemplate);
            Y.one('.self-btn').on('click', function () {
                self.setBrowseHandler();
                self.showPersonalGames();
            });
        },

        createStandbyGame: function () {
            var newGame = new Game({
                players: [this.get('user').get('id')],
                types: {
                    hint_type: 'gene',
                    guess_type: 'disease'
                },
                identifiers: {
                    hint_id: 'entrez',
                    guess_id: 'odo'
                }
            }),
                self = this;
            Y.one('#main-container').empty();
            newGame.save(function () {
                self.get('gameList').load(function () {
                    self.render();
                });
            });
        },

        showStandbyGames: function () {
            var gameList = this.get('gameList'),
                self     = this;

            this.set('notUserQuery', this.get('userQuery'));
            this.set('userQuery', null);
            this.set('infoQuery', 'standby');
            this.set('mode', 'browse');

            Y.one('#main-container').empty();
            gameList.load({info: this.get('infoQuery')}, function () {
                self.render();   
            });
        },

        showPersonalGames: function () {
            var gameList = this.get('gameList'),
                self     = this;

            this.set('userQuery', this.get('notUserQuery'));
            this.set('notUserQuery', null);
            this.set('infoQuery', 'start');
            this.set('mode', 'self');

            Y.one('#main-container').empty();
            gameList.load({user: this.get('userQuery'), info: 'start'}, function () {
                self.render();
            });
            complete: { value: false }
        },

        joinStandbyGame: function (e) {
            var self = this,
                gameId = e.target.getData('game-id'),
                opponentId = e.target.getData('user-id'),
                user = this.get('user'),
                opponent = new User({id: parseInt(opponentId)}),
                game = new Game({id: gameId});


            game.load(function () {
                opponent.load(function () {
                    var players    = game.get('players'),
                        userId     = user.get('id'),
                        opponentId = opponent.get('id'),
                        round      = new Round({
                            game_id: game.get('id'),
                            players: {
                                hinter: user.get('name'),
                                guesser: opponent.get('name')
                            },
                            turn: user.get('name')
                        });

                    players.push(userId);

                    game.set('players', players);
                    game.set('info', 'start');

                    game.save(function () {
                        round.save(function () {
                            window.clearInterval(self.get('dashId'));
                            new GameView({
                                game     : game,
                                player   : user,
                                opponent : opponent,
                                round    : round
                            });
                        });
                    });
                });
            });
        },

        joinStartedGame: function (e) {
            var self   = this,
                user   = this.get('user'),
                gameId = e.target.getData('game-id'),
                game   = new Game({id: gameId});

            window.clearInterval(self.get('dashId'));
            game.load(function () {
                var players    = game.get('players'),
                    opponentId,
                    opponent;

                opponentId = user.get('id') == players[0] ? players[1] : players[0];
                opponent = new User({id: opponentId});

                opponent.load(function () {
                    var rounds        = game.get('rounds'),
                        lastRoundData = rounds[rounds.length - 1];
                        round         = new Round(lastRoundData);

                    new GameView({
                        game: game,
                        player: user,
                        opponent: opponent,
                        round: round
                    });
                });
            });
        },

        pollForData: function () {
            var self = this;

            var dashId = window.setInterval(function () {
                self.get('gameList').load({
                    user: self.get('userQuery'),
                    not_user: self.get('notUserQuery'),
                    info: self.get('infoQuery')
                }, function () {
                    Y.one('#main-container').empty();
                    self.render();
                });
            }, 3000);
            this.set('dashId', dashId);
        }
        
    }, {
        ATTRS: {
            container: {
                valueFn: function () {
                    return Y.one('body');
                }
            },
            
            userQuery: {
                value: null
            },

            infoQuery: {
                value: null
            },

            notUserQuery: {
                value: null
            }
        }
    });    
    
    new HomeView();
});
